/**
 * 函数防抖
 * @param {Function} func 要防抖动的函数
 * @param {number} [wait=0] 需要延迟的毫秒数
 * @param {object} [options={}] 选项对象
 * @param {boolean} [options.leading=false] 指定在延迟开始前调用
 * @param {number} [options.maxWait] 允许被延迟的最大值
 * @param {boolean} [options.trailing=true] 指定在延迟结束后调用
 * @returns 返回新的 debounced（防抖动）函数
 */
export function debounce(func, wait, options) {
  let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;
  let lastInvokeTime = 0;
  let leading = false;
  let maxing = false;
  let trailing = true;
  // 函数类型校验
  if (typeof func != 'function') throw new TypeError('请传入函数');
  wait = Number(wait) || 0;
  if (typeof options === 'object' && options !== null) {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    maxWait = maxing ? Math.max(Number(options.maxWait) || 0, wait) : maxWait;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  // 调用 func 函数
  function invokeFunc(time) {
    const args = lastArgs;
    const thisArg = lastThis;
    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }
  // 处理 leading 情况
  function leadingEdge(time) {
    // 重置任何 maxWait 定时器
    lastInvokeTime = time;
    // 启动 trailing 定时器
    timerId = setTimeout(timerExpired, wait);
    // 如果 leading 为 true，则立即调用 func 函数
    return leading ? invokeFunc(time) : result;
  }
  // 计算剩余等待时间
  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime;
    const timeSinceLastInvoke = time - lastInvokeTime;
    const timeWaiting = wait - timeSinceLastCall;
    return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
  }
  // 判断是否应该调用 func 函数
  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime;
    const timeSinceLastInvoke = time - lastInvokeTime;
    // 如果是第一次调用或者距离上次调用已经超过wait时间，则应该调用func函数
    return (
      lastCallTime === undefined ||
      timeSinceLastCall >= wait ||
      timeSinceLastCall < 0 ||
      (maxing && timeSinceLastInvoke >= maxWait)
    );
  }
  // 处理定时器到期的情况
  function timerExpired() {
    const time = Date.now();
    // 如果应该调用 func 函数，则调用 trailingEdge 函数
    if (shouldInvoke(time)) return trailingEdge(time);
    // 否则重新启动定时器
    timerId = setTimeout(timerExpired, remainingWait(time));
  }
  // 用于处理 trailing 情况
  function trailingEdge(time) {
    timerId = undefined;
    // 如果 lastArgs 存在，则调用 func 函数
    if (trailing && lastArgs) return invokeFunc(time);
    lastArgs = lastThis = undefined;
    return result;
  }
  // debounced 函数，用于返回防抖函数
  function debounced() {
    const time = Date.now();
    const isInvoking = shouldInvoke(time);
    lastArgs = arguments;
    // @ts-ignore
    lastThis = this;
    lastCallTime = time;
    if (isInvoking) {
      if (timerId === undefined) return leadingEdge(lastCallTime);
      if (maxing) {
        // 在紧密循环中处理调用
        clearTimeout(timerId);
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) timerId = setTimeout(timerExpired, wait);
    return result;
  }
  // 取消执行
  debounced.cancel = function () {
    if (timerId !== undefined) clearTimeout(timerId);
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  };
  // 立即执行
  debounced.flush = function () {
    return timerId === undefined ? result : trailingEdge(Date.now());
  };
  // 检查当前是否在计时中
  debounced.pending = function () {
    return timerId !== undefined;
  };
  return debounced;
}

/**
 * 函数节流（maxWait 为 wait 的防抖函数）
 * @param {Function} func 要节流的函数
 * @param {number} [wait=0] 需要节流的毫秒
 * @param {object} [options={}] 选项对象
 * @param {boolean} [options.leading=true] 指定调用在节流开始前
 * @param {boolean} [options.trailing=true] 指定调用在节流结束后
 * @returns 返回节流的函数
 */
export function throttle(func, wait, options) {
  let leading = true;
  let trailing = true;
  // 函数类型校验
  if (typeof func != 'function') throw new TypeError('请传入函数');
  if (typeof options === 'object' && options !== null) {
    leading = 'leading' in options ? !!options.leading : leading;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
    leading: leading,
    maxWait: wait,
    trailing: trailing,
  });
}

/**
 * 获取对象差异
 * @param {*} obj 旧对象
 * @param {*} newObj 新对象
 * @returns {array} 差异数组
 */
export const diff = (obj, newObj, options = { cyclesFix: true }, _stack = []) => {
  const richTypes = { Date: 1, RegExp: 1, String: 1, Number: 1 };
  const diffs = [];
  if (!obj) return diffs;
  const isObjArray = Array.isArray(obj);
  for (const key in obj) {
    const objKey = obj[key];
    const path = isObjArray ? +key : key;
    if (!(key in newObj)) {
      diffs.push({
        type: 'delete',
        path: [path],
        oldValue: objKey,
      });
      continue;
    }
    const newObjKey = newObj[key];
    const areObjects = typeof objKey === 'object' && typeof newObjKey === 'object';
    if (
      objKey &&
      newObjKey &&
      areObjects &&
      !richTypes[Object.getPrototypeOf(objKey).constructor.name] &&
      (options.cyclesFix ? !_stack.includes(objKey) : true)
    ) {
      const nestedDiffs = diff(objKey, newObjKey, options, options.cyclesFix ? _stack.concat([objKey]) : []);
      diffs.push.apply(
        diffs,
        nestedDiffs.map((difference) => {
          difference.path.unshift(path);
          return difference;
        }),
      );
    } else if (
      objKey !== newObjKey &&
      !(areObjects && (Number.isNaN(objKey) ? objKey + '' === newObjKey + '' : +objKey === +newObjKey))
    ) {
      diffs.push({
        type: 'modify',
        path: [path],
        value: newObjKey,
        oldValue: objKey,
      });
    }
  }
  const isNewObjArray = Array.isArray(newObj);
  for (const key in newObj) {
    if (!(key in obj)) {
      diffs.push({
        type: 'create',
        path: [isNewObjArray ? +key : key],
        value: newObj[key],
      });
    }
  }
  return diffs;
};

/**
 * 获取用户当前操作系统类型
 * @returns {'windows' | 'mac' | 'linux' | 'android' | 'ios' | 'unknow'}
 */
export const getOSType = () => {
  const userAgent = window.navigator.userAgent;
  switch (true) {
    case /Win/i.test(userAgent):
      return 'windows';
    case /Mac/i.test(userAgent):
      return 'mac';
    case /Linux/i.test(userAgent):
      return 'linux';
    case /Android/i.test(userAgent):
      return 'android';
    case /(iPhone|iPod|iPad)/i.test(userAgent):
      return 'ios';
    default:
      return 'unknow';
  }
};
